﻿using System;
using System.Collections.Generic;
using System.Text;
using log4net.Layout;
using log4net.Core;
using log4net.Util;
using System.Net;
using System.Net.Sockets;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Threading;
using System.Net.NetworkInformation;
using System.Diagnostics;
using log4net.Appender;
using DNSharp;

namespace Splunk.Extensions
{
    /// <summary>
    /// Enumeration for the load balance method. 
    /// </summary>
    public enum LoadBalanceMode:int
    {
        /// <summary>
        /// Each message is directed to a different host if available. 
        /// </summary>
        RoundRobin=1,
        /// <summary>
        /// Messages stay on the same server until the server is marked as failed.
        /// </summary>
        Persistent=2
    }

    /// <summary>
    /// This class acts as the base class for all dns configurable appenders.
    /// </summary>
    public abstract class BaseDNSAppender:AppenderSkeleton
    {
        private LoadBalanceMode _LoadBalanceMode;
        /// <summary>
        /// .ctor
        /// </summary>
        /// <param name="mode"></param>
        public BaseDNSAppender(LoadBalanceMode mode)
        {
            _LoadBalanceMode = mode;
        }

        /// <summary>
        /// Maximum number of queued events.
        /// </summary>
        protected const int MAXQUEUED = 5000;

        private string _DnsRecord;
        private static readonly TimeSpan MonitorWaitInterval = new TimeSpan(0, 15, 0);
        private ManualResetEvent MonitorThreadExitSignal = new ManualResetEvent(false);
        private Thread _MonitorThread;
        private bool _Initialized = false;
        private IPEndPoint[] _SplunkEndPoints = null;
        private List<byte[]> _QueuedMessages = new List<byte[]>();
        /// <summary>
        /// This value is used internally to determine which endpoint to send data to. This is not 
        /// a true message count and should not be used as one. 
        /// </summary>
        private long _MessageCount = 0;

        /// <summary>
        /// DNS record to lookup
        /// </summary>
        public string DnsRecord
        {
            get { return _DnsRecord; }
            set { _DnsRecord = value; }
        }
        /// <summary>
        /// Flag to determine if layout is required
        /// </summary>
        protected override bool RequiresLayout
        {
            get { return true; }
        }

        private IPEndPoint[] GetHosts()
        {
            if (LogLog.IsDebugEnabled)
                LogLog.Debug(string.Format("BaseDNSAppender:Looking up SRV record {0}...", _DnsRecord));

            DnsResourceRecordSRV[] records = null;

            Stopwatch watch = LogLog.IsDebugEnabled ? Stopwatch.StartNew() : null;
            try
            {
                Resolver resolver = new Resolver();
                records = resolver.QuerySRVRecord(_DnsRecord);
            }
            catch (Exception ex)
            {
                if (LogLog.IsErrorEnabled)
                    LogLog.Error("BaseDNSAppender:Exception thrown getting SRV records", ex);
                throw;
            }
            if (LogLog.IsDebugEnabled && null != watch)
            {
                watch.Stop();
                LogLog.Debug(
                    string.Format("DnsResolver.LookupSRV took {0} to resolve.", watch.Elapsed)
                    );
            }


            if (LogLog.IsDebugEnabled) LogLog.Debug(
                string.Format("BaseDNSAppender:Found {0} SRV records", records.Length)
                );
            List<IPEndPoint> SplunkEndPoints = new List<IPEndPoint>();

            foreach (DnsResourceRecordSRV srvRecord in records)
            {
                try
                {
                    IPAddress ip = null;
                    if (LogLog.IsDebugEnabled) LogLog.Debug(
                        string.Format("BaseDNSAppender:Parsing \"{0}\" to IP Address.", srvRecord.Target)
                        );
                    if (!IPAddress.TryParse(srvRecord.Target, out ip))
                    {
                        if (LogLog.IsDebugEnabled) LogLog.Debug(
                            string.Format("BaseDNSAppender:\"{0}\" is not a IP Address, Looking up with Dns.GetHostEntry(\"{0}\")", srvRecord.Target)
                            );
                        IPHostEntry hostEntry = Dns.GetHostEntry(srvRecord.Target);
                        if (hostEntry.AddressList.Length == 0)
                        {
                            if (LogLog.IsWarnEnabled)
                                LogLog.Warn(
                                    string.Format("BaseDNSAppender:Nothing was returned for \"{0}\"", srvRecord.Target)
                                    );
                        }
                        else
                            ip = hostEntry.AddressList[0];
                        if (LogLog.IsDebugEnabled) LogLog.Debug(
                            string.Format("BaseDNSAppender:\"{0}\" resolved to {1}", srvRecord.Target, ip)
                            );
                    }

                    IPEndPoint ep = new IPEndPoint(ip, srvRecord.Port);
                    if (LogLog.IsDebugEnabled) LogLog.Debug(
                        string.Format("BaseDNSAppender:Adding endpoint of \"{0}\" to endpoints.", ep)
                        );
                    SplunkEndPoints.Add(ep);
                }
                catch (Exception ex)
                {
                    if (LogLog.IsErrorEnabled)
                        LogLog.Error("BaseDNSAppender:Exception Throw finding EndPoints", ex);
                }
            }

            if (LogLog.IsDebugEnabled) LogLog.Debug(
                string.Format("BaseDNSAppender:Found Total of {0} splunk endpoints.", SplunkEndPoints.Count)
                );
            return SplunkEndPoints.ToArray();
        }
        /// <summary>
        /// Shutdown the appender 
        /// </summary>
        protected override void OnClose()
        {
            MonitorThreadExitSignal.Set();
        }
        /// <summary>
        /// Startup the appender
        /// </summary>
        public override void ActivateOptions()
        {
            base.ActivateOptions();
            _MonitorThread = new Thread(new ThreadStart(monitor));
            _MonitorThread.IsBackground = true;
            _MonitorThread.Start();
        }

        /// <summary>
        /// Method is used to ping the remote host and ensure that 
        /// the remote host is available. This is required since all
        /// traffic is UDP.
        /// </summary>
        /// <param name="splunkHost">Host to validate</param>
        /// <returns>True if the host is pingable, false if not.</returns>
        protected static bool IsPingable(IPEndPoint splunkHost)
        {
            return IsPingable(splunkHost.Address);
        }
        /// <summary>
        /// Method is used to ping the remote host and ensure that 
        /// the remote host is available. This is required since all
        /// traffic is UDP.
        /// </summary>
        /// <param name="splunkHost">Host to validate</param>
        /// <returns>True if the host is pingable, false if not.</returns>
        protected static bool IsPingable(IPAddress splunkHost)
        {
            using (Ping ping = new Ping())
            {
                PingReply reply = ping.Send(splunkHost, 500);
                return (reply.Status == IPStatus.Success);
            }
        }
        /// <summary>
        /// Method is used to initialize the appender
        /// </summary>
        /// <returns></returns>
        protected abstract bool Initialize();

        private void sendQueuedMessages()
        {
            lock (_QueuedMessages)
            {
                foreach (byte[] message in _QueuedMessages)
                {
                    SendMessage(message);
                }
                _QueuedMessages.Clear();
            }

        }

        private void monitor()
        {
            while (true)
            {
                try
                {
                    if (!_Initialized)
                    {
                        _Initialized = Initialize();
                        updateSplunkEndpoints();
                    }
                    else
                    {
                        updateSplunkEndpoints();
                    }
                }
                catch (Exception ex)
                {
                    if (!LogLog.QuietMode)
                        LogLog.Error("BaseDNSAppender:Failed to create socket", ex);
                }

                try
                {
                    if (_Initialized)
                        sendQueuedMessages();
                }
                catch (Exception ex)
                {
                    if (!LogLog.QuietMode)
                        LogLog.Error("BaseDNSAppender:Exception sending queued messages", ex);
                }


                //Check hosts every 15 minutes.
                if (LogLog.IsDebugEnabled) LogLog.Debug(
                    string.Format("BaseDNSAppender:Waiting {0} to perform host checks again.", MonitorWaitInterval)
                    );
                if (MonitorThreadExitSignal.WaitOne(MonitorWaitInterval, false))
                    break;
            }
        }

        private void updateSplunkEndpoints()
        {
            if (LogLog.IsDebugEnabled) LogLog.Debug(
                string.Format("BaseDNSAppender:Checking for updated hosts in DNS.")
                );
            IPEndPoint[] endpoints = GetHosts();

            if (endpoints.Length > 0)
            {
                Interlocked.Exchange<IPEndPoint[]>(ref _SplunkEndPoints, endpoints);
            }
        }

        /// <summary>
        /// Method is used to send data. Child class should take the message and send to an endpoint.
        /// </summary>
        /// <param name="message"></param>
        protected abstract void SendMessage(byte[] message);

        /// <summary>
        /// Method is used to write an event
        /// </summary>
        /// <param name="loggingEvent"></param>
        protected override void Append(LoggingEvent loggingEvent)
        {
            string Message = RenderLoggingEvent(loggingEvent);
            if (string.IsNullOrEmpty(Message))
                return;
            Message = Message.Trim();
            byte[] buffer = Encoding.UTF8.GetBytes(Message + "\r\n\r\n");
            if (!_Initialized ||null == _SplunkEndPoints)
            {
                QueueMessage(buffer);
                return;
            }

            SendMessage(buffer);
        }

        /// <summary>
        /// Method is used to queue a message during startup. 
        /// </summary>
        /// <param name="buffer">Message buffer to queue</param>
        protected void QueueMessage(byte[] buffer)
        {
            if (_QueuedMessages.Count <= MAXQUEUED)
                _QueuedMessages.Add(buffer);
        }

        /// <summary>
        /// Method is used to queue a message during startup.
        /// </summary>
        /// <param name="format">format for the message.</param>
        /// <param name="args">arguments</param>
        protected void QueueMessage(string format, params object[] args)
        {
            string message = null;

            if (null == args || args.Length == 0)
            {
                message = format;
            }
            else
            {
                message = string.Format(format, args);
            }

            if (string.IsNullOrEmpty(message))
                return;

            byte[] buffer = Encoding.UTF8.GetBytes(message);
            QueueMessage(buffer);
        }



        private TimeSpan FailedEndpointTimeout=new TimeSpan(0,5,0);
        private Dictionary<IPEndPoint, DateTime> FailedEndpointLookup = new Dictionary<IPEndPoint, DateTime>();
        
        /// <summary>
        /// Method is used to mark an endpoint as failed. This will take the endpoint out of rotation for 5 minutes. 
        /// </summary>
        /// <param name="endpoint"></param>
        protected void MarkEndPointFailure(IPEndPoint endpoint)
        {
            if (LogLog.IsWarnEnabled) LogLog.Warn(
                string.Format("Marking {0} as failed. {0} will be ignored for {1}", endpoint, FailedEndpointTimeout)
                );
            lock (FailedEndpointLookup)
            {
                if (!FailedEndpointLookup.ContainsKey(endpoint))
                    FailedEndpointLookup.Add(endpoint, DateTime.Now);
            }
        }

        IPEndPoint GetRoundRobinEndPoint()
        {
            return GetNextEndPoint();
        }

        object _PersistentEndPointLock = new object();
        IPEndPoint _PersistentEndPoint = null;

        /// <summary>
        /// Method is used to return a persistent endpoint. 
        /// </summary>
        /// <returns></returns>
        IPEndPoint GetPersistentEndPoint()
        {
            lock (_PersistentEndPointLock)
            {
                if (null == _PersistentEndPoint)
                {
                    _PersistentEndPoint = GetNextEndPoint();
                }

                if (IsValidEndPoint(_PersistentEndPoint))
                {
                    _PersistentEndPoint = GetNextEndPoint();
                }

                return _PersistentEndPoint;
            }
        }

        /// <summary>
        /// Method will return an endpoint that the inherited code should send data to.
        /// </summary>
        /// <returns>Endpoint</returns>
        protected IPEndPoint GetEndPoint()
        {
            IPEndPoint endpoint = null;

            switch (_LoadBalanceMode)
            {
                case LoadBalanceMode.Persistent:
                    endpoint = GetPersistentEndPoint();
                    break;
                case LoadBalanceMode.RoundRobin:
                    endpoint = GetRoundRobinEndPoint();
                    break;
                default:
                    throw new NotSupportedException(string.Format("LoadBalanceMode of {0} is not supported.", _LoadBalanceMode));
            }

            return endpoint;
        }

        /// <summary>
        /// Method is used to check if the endpoint is valid. This method will cleanup the lookup of failed endpoint. 
        /// This method will also check the list of hosts to ensure that the host is still valid. 
        /// </summary>
        /// <param name="EndPoint">Endpoint to check</param>
        /// <returns>True if the endpoint has failed, false if not.</returns>
        protected bool IsValidEndPoint(IPEndPoint EndPoint)
        {
            DateTime lastFailed = DateTime.MinValue;
            if (FailedEndpointLookup.TryGetValue(EndPoint, out lastFailed))
            {
                lock (FailedEndpointLookup)
                {
                    if ((DateTime.Now - lastFailed) > FailedEndpointTimeout)
                    {
                        try
                        {
                            FailedEndpointLookup.Remove(EndPoint);
                        }
                        catch (Exception ex)
                        {
                            LogLog.Error("Exception while removing failed endpoint.", ex);
                        }
                    }
                    else
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// Method is used to return the next available endpoint from the list.
        /// </summary>
        /// <returns>Endpoint</returns>
        private IPEndPoint GetNextEndPoint()
        {
            int EndpointIndex = (int)(_MessageCount++ % _SplunkEndPoints.LongLength);
            IPEndPoint ep = _SplunkEndPoints[EndpointIndex];

            if (IsValidEndPoint(ep))
            {
                EndpointIndex = (int)(_MessageCount++ % _SplunkEndPoints.LongLength);
                ep = _SplunkEndPoints[EndpointIndex];
            }

            return ep;
        }
    }
}
